﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Windows.Forms;
using System.Drawing.Drawing2D;
using System.Drawing.Text;

namespace EbnfStudio
{

    /// <summary>
    /// Visual graph generator
    /// </summary>
    class GraphVisual : IGraphVisitor, IGraphTreeConfig
    {

        /// <summary>
        /// The resulting image bitmap
        /// </summary>
        public Bitmap Bitmap { get; private set; }

        #region IGraphTreeConfig
        /// <summary>
        /// Get size of the token
        /// </summary>
        public Size GetSize(Token token)
        {
            switch (token.Id)
            {
                case TokenClass.Literal:
                    {
                        // male it atleast one char long
                        var str = (token.Lexeme.Length > 0) ? token.Lexeme : " ";
                        var sf = ctx.MeasureString(str, LitFont, new PointF(0, 0), LitStrFmt);
                        var size = new Size((int)sf.Width, (int)sf.Height);
                        size.Width += LitPadding * 2;
                        size.Height += LitPadding * 2;
                        return size;
                    }
                case TokenClass.Identifier:
                    {
                        Size size;
                        var sf = ctx.MeasureString(token.ToString(), idFont, new PointF(0, 0), idStrFmt);
                        size = new Size((int)sf.Width, (int)sf.Height);
                        size.Width += IdPadding * 2;
                        size.Height += IdPadding * 2;
                        return size;
                    }
            }

            // default
            return new Size(0, 0);
        }


        /// <summary>
        /// Get size of the token by class
        /// </summary>
        public Size GetSize(TokenClass id)
        {
            switch (id)
            {
                case TokenClass.Empty:
                    return GetSize(new Token(TokenClass.Literal, " ", 0, 0, 0));
            }

            // no size.
            return new Size(0, 0);
        }


        /// <summary>
        /// Get size of the Syntax tree
        /// </summary>
        /// <param name="count">Number of rules within syntax tree</param>
        /// <param name="size">total size of all rules combined</param>
        /// <param name="max">maximum width and height of the contained rules</param>
        public Size GetSyntaxSize(int count, Size size, Size max)
        {
            /*
             * Rule
             * 
             * Rule
             */
            return new Size(
                max.Width + (SyntaxMargin * 2) + 1 + 5,
                size.Height + ((count - 1) * RuleSpacer) + (SyntaxMargin * 2) + 1
            );
        }

        /// <summary>
        /// Get rule size
        /// </summary>
        /// <param name="maxIdSize">the maximum id size</param>
        /// <param name="exprSize">size of the contained expression</param>
        public Size GetRuleSize(Size maxIdSize, Size exprSize)
        {
            /*
             * Identifier = Expression;
             */
            return new Size(
                maxIdSize.Width + RuleIdSpacer + exprSize.Width,
                exprSize.Height + (maxIdSize.Height / 2) + 1
            );
        }


        /// <summary>
        /// get size of the expression
        /// </summary>
        /// <param name="count">number terminals contained</param>
        /// <param name="size">Total size of the terminals</param>
        /// <param name="max">Maximum terminal size</param>
        /// <returns>Size of the expression block</returns>
        public Size GetExprSize(int count, Size size, Size max)
        {
            /*
             * Term->-Term
             */
            return new Size(size.Width + ((count - 1) * GenSpacer), max.Height);
        }


        /// <summary>
        /// Get size for the Term.
        /// </summary>
        /// <param name="count">Number of factors inside the terminal</param>
        /// <param name="size">Size of all factors combined</param>
        /// <param name="max">Max width and height of all terminals contained</param>
        public Size GetTermSize(int count, Size size, Size max)
        {
            // just one item
            if (count == 1) return new Size(max.Width, size.Height);

            /*
             * +->-factor->-+
             * |            |
             * +->-factor->-+
             */
            return new Size(max.Width + (GenSpacer * 2), size.Height + (count - 1) * FactorSpacer);
        }


        /// <summary>
        /// Get size of the group
        /// </summary>
        /// <param name="size">Size of the containes expression</param>
        public Size GetGroupSize(Size size)
        {

            if (size.Width == 0 && size.Height == 0)
            {
                return GetSize(TokenClass.Empty);
            }

            /*
             * Expression
             */
            return new Size(size.Width, size.Height);
        }


        /// <summary>
        /// Get size of the optional group
        /// </summary>
        /// <param name="size">size of the expression contained</param>
        public Size GetOptionalSize(Size size)
        {
            /*
             * +------->--------+
             * |                |
             * +->-Expression->-+
             */
            //return new Size(size.Width + 8, size.Height + 2);
            return new Size(size.Width + (GenSpacer * 2), size.Height + FactorSpacer + GetSize(TokenClass.Empty).Height);
        }


        /// <summary>
        /// get size of the repetition group
        /// </summary>
        /// <param name="size">size of the expression contained</param>
        public Size GetRepetitionSize(Size size)
        {
            /*
             * +------->--------+
             * |                |
             * +-<-Expression-<-+
             */
            return new Size(size.Width + (GenSpacer * 2), size.Height + FactorSpacer + GetSize(TokenClass.Empty).Height);
        }
        #endregion

        #region SizeConfig

        // margin around entire syntax tree
        int SyntaxMargin = 5;
        // number of pixels between the rules
        int RuleSpacer = 5;
        // number of pixels between factors
        int FactorSpacer = 5;
        // width between rule id and an expression
        int RuleIdSpacer = 10;
        // Identifier padding
        int IdPadding = 1;
        // padding for litetals
        int LitPadding = 1;
        // number of pixels between the items (might include an arrow!)
        // so must be > ArrowWidth
        int GenSpacer = 26;
        // the width (and relative height) of the arrow
        int ArrowWidth = 9;
        // line margin
        int LineMargin = 10;
        #endregion

        #region LocalVars

        /// the context
        Graphics ctx = null;

        // current x and y positions
        int XPos = 0, YPos = 0, LineY = 0;

        // identifier
        Brush           idBrush;
        Pen             idPen;
        Brush           idFontBrush;
        StringFormat    idStrFmt;
        Font            idFont;

        // Literals
        Brush           LitBrush;
        Pen             LitPen;
        Brush           LitErrorBrush;
        Pen             LitErrorPen;
        Font            LitFont;
        Brush           LitFontBrush;
        Brush           LitErrorFontBrush;
        StringFormat    LitStrFmt;

        // general
        Pen             linePen;
        Brush           lineBrush;

        // direction of arrows
        bool ForwardArrows = true;

        #endregion

        /// <summary>
        /// construct
        /// </summary>
        public GraphVisual(AstSyntax syntax)
        {

            // create temporary bitmap
            Bitmap = new Bitmap(1, 1);

            using (idBrush      = new SolidBrush(Color.White))
            using (idFontBrush  = new SolidBrush(Color.Black))
            using (idStrFmt     = new StringFormat())
            using (idPen        = new Pen(Color.Black))
            using (idFont       = new Font("Courier New", 14, GraphicsUnit.Pixel))
            using (LitBrush     = new SolidBrush(Color.White))
            using (LitPen       = new Pen(Color.Black))
            using (LitFont      = new Font("Courier New", 14, GraphicsUnit.Pixel))
            using (LitFontBrush = new SolidBrush(Color.Blue))
            using (LitStrFmt    = new StringFormat())
            using (linePen      = new Pen(Color.Black))
            using (lineBrush    = new SolidBrush(Color.White))
            using (LitErrorBrush= new SolidBrush(Color.Yellow))
            using (LitErrorPen  = new Pen(Color.Red))
            using (LitErrorFontBrush= new SolidBrush(Color.Red))
            using (ctx          = GetCanvas())
            {
                // setup general
                linePen.Width = 1;

                // setup identifier
                idPen.Width = 1;
                idStrFmt.Trimming = StringTrimming.None;
                idStrFmt.Alignment = StringAlignment.Center;
                idStrFmt.LineAlignment = StringAlignment.Center;
                idStrFmt.FormatFlags = StringFormatFlags.NoWrap
                                     | StringFormatFlags.MeasureTrailingSpaces
                                     ;
                

                // set up literal
                LitPen.Width = 1;
                LitStrFmt.Dispose();
                LitStrFmt = (StringFormat)idStrFmt.Clone();

                // Walk the ast tree and generate graph tree
                var visualyzer = new GraphTree((IGraphTreeConfig)this);
                syntax.Accept(visualyzer);

                // the size of entire syntax
                var size = visualyzer.SyntaxTree.Size;
                if (size.Width == 0 || size.Height == 0)
                {
                    Bitmap.Dispose();
                    Bitmap = null;
                    return;
                }
                
                // create real bitmap. This is needed because
                // TextRenderer.MeasureText gives bogus and unreliable
                // text sizes. Need Graphics.MeasureString method!
                Bitmap.Dispose();
                ctx.Dispose();
                Bitmap = new Bitmap(size.Width, size.Height);
                ctx = GetCanvas();

                // draw outline
                ctx.Clear(Color.White);
                var pen = (Pen)Pens.Green.Clone();
                pen.Width = 2;
                // ctx.DrawRectangle(pen, 0, 0, size.Width - 1, size.Height - 1);
                
                // visualize.
                visualyzer.SyntaxTree.Accept((IGraphVisitor)this);
            }
        }


        /// <summary>
        /// get the canvas element
        /// </summary>
        Graphics GetCanvas()
        {
            var canvas = Graphics.FromImage(Bitmap);
            canvas.SmoothingMode = SmoothingMode.HighQuality;
            return canvas;
        }


        /// Code for IGraphVisitor
        #region IGraphVisitor

        /// <summary>
        /// The entire syntax tree
        /// </summary>
        public void Visit(GraphSyntaxElement ast)
        {
            int x = XPos, y = YPos;
            foreach(var rule in ast.Rules)
            {
                // set positions
                XPos = x + SyntaxMargin; YPos = y + SyntaxMargin;

                // visit the rule
                rule.Accept(this);

                var lx1 = x + SyntaxMargin + rule.Size.Width;
                var ly1 = y + SyntaxMargin + rule.Identifier.Size.Height;
                var lx2 = ast.Size.Width - SyntaxMargin-1;
                // var ly2 = ly1;
                ctx.DrawLine(linePen, lx1, ly1, lx2, ly1);
                ctx.DrawLine(linePen, lx2, ly1-3, lx2, ly1+3);

                // increase y
                y += rule.Size.Height + this.RuleSpacer;
            }
        }


        /// <summary>
        /// single rule
        /// </summary>
        public void Visit(GraphRuleElement ast)
        {
            var x = XPos; var y = YPos;
            // identifier
            ast.Identifier.Accept(this);

            // draw the line running through the graph
            var idSize = ast.Identifier.Size;
            int l_x = x + idSize.Width;
            int l_y = y + idSize.Height;
            int l_x2 = l_x + ast.Size.Width - ast.Identifier.Size.Width;
            int l_y2 = l_y;
            ctx.DrawLine(linePen, l_x-2, l_y, l_x2, l_y2);
            // ctx.DrawLine(linePen, l_x2, l_y - 3, l_x2, l_y + 3);

            LineMargin = idSize.Height / 2;

            // reset x and y position
            XPos = l_x + RuleIdSpacer;
            YPos = l_y - LineMargin;
            LineY = l_y;

            // render children
            ast.Expression.Accept(this);
        }


        /// <summary>
        /// expression in the rule
        /// </summary>
        public void Visit(GraphExpressionElement ast)
        {
            int cnt = ast.Terms.Count;
            for (int i = 0; i < cnt; i++)
            {
                // current line and position
                var x = XPos;
                var y = YPos;
                var liney = LineY;

                // display terminal
                var term = ast.Terms[i];
                term.Accept(this);

                // restore cursor position
                XPos = x + term.Size.Width;
                YPos = y;
                LineY = liney;

                // add a glue
                if (i < (cnt - 1)) DrawArrow(GenSpacer, true);
            }
        }


        /// <summary>
        /// Single terminal
        /// </summary>
        public void Visit(GraphTermElement ast)
        {
            // only a single
            if (ast.Factors.Count == 1)
            {
                var factor = ast.Factors[0];
                factor.Accept(this);
                return;
            }

            /*
             * +->-factor->-+
             * |            |
             * +->-factor->-+
             */
            int y2 = LineY + ast.Size.Height - ast.Factors[ast.Factors.Count - 1].Size.Height;
            ctx.DrawLine(linePen, XPos, LineY, XPos, y2);
            ctx.DrawLine(linePen, XPos + ast.Size.Width, LineY, XPos + ast.Size.Width, y2);

            foreach (var factor in ast.Factors)
            {
                var x = XPos; var y = YPos; var liney = LineY;

                // base line and arrow
                ctx.DrawLine(linePen, XPos, liney, XPos + ast.Size.Width, liney);
                DrawArrow(GenSpacer, true);

                // render child
                factor.Accept(this);
                
                // post arrow
                XPos = x + ast.Size.Width - GenSpacer;
                YPos = y; LineY = liney;
                DrawArrow(GenSpacer, true);

                // new line
                XPos = x;
                YPos = y + factor.Size.Height + FactorSpacer;
                LineY = YPos + LineMargin;
            }
        }


        /// <summary>
        /// empty production
        /// </summary>
        public void Visit(GraphEmptyFactorElement ast)
        {
        }


        /// <summary>
        /// the identifier
        /// </summary>
        public void Visit(GraphIdentifierElement ast)
        {
            // vars. shorthand
            var token = ast.Identifier;
            var size = ast.Size;
            var rect = new Rectangle(XPos, YPos, size.Width, size.Height);

            // the box and the border
            ctx.FillRectangle(idBrush, rect);
            ctx.DrawRectangle(idPen, rect);
            
            // draw the string
            ctx.DrawString(token.ToString(), idFont, idFontBrush, rect, idStrFmt);
        }


        /// <summary>
        /// the literal
        /// </summary>
        public void Visit(GraphLiteralElement ast)
        {
            // vars. shorthand
            var token = ast.Literal;
            var size = ast.Size;
            var rect = new Rectangle(XPos, YPos, size.Width, size.Height);
            var path = RoundedRectangle.Create(rect, 6);

            if (token.ErrorCode == TokenError.NoError)
            {
                // the box and the border
                ctx.FillPath(LitBrush, path);
                ctx.DrawPath(LitPen, path);

                // draw the string
                ctx.DrawString(token.Lexeme, LitFont, LitFontBrush, rect, LitStrFmt);
            }
            else
            {
                // the box and the border
                ctx.FillPath(LitErrorBrush, path);
                ctx.DrawPath(LitErrorPen, path);

                // draw the string
                ctx.DrawString(token.Lexeme, LitFont, LitErrorFontBrush, rect, LitStrFmt);
            }
        }


        /// <summary>
        /// the group ()
        /// </summary>
        public void Visit(GraphGroupElement ast)
        {
            ast.Expression.Accept(this);
        }


        /// <summary>
        /// the optional []
        /// </summary>
        public void Visit(GraphOptionalElement ast)
        {
            /*
             * +------->--------+
             * |                |
             * +->-Expression->-+
             */

            // temporaries
            var x = XPos; var y = YPos; var liney = LineY;
            var x2 = x + ast.Expression.Size.Width + GenSpacer * 2;
            var liney2 = liney + FactorSpacer + GetSize(TokenClass.Empty).Height;
            
            // the lines
            ctx.DrawLine(linePen, x, liney, x, liney2);
            ctx.DrawLine(linePen, x2, liney, x2, liney2);
            ctx.DrawLine(linePen, x, liney2, x2, liney2);

            // empty line arrow
            XPos += (ast.Size.Width / 2 - GenSpacer / 2);
            DrawArrow(GenSpacer, true);

            // expression left arrow
            XPos = x; YPos = liney2 - LineMargin; LineY = liney2;
            DrawArrow(GenSpacer, true);

            // the expression itself
            ast.Expression.Accept(this);

            // the right side arrow
            XPos = x + GenSpacer + ast.Expression.Size.Width;
            YPos = liney2 - LineMargin;
            LineY = liney2;
            DrawArrow(GenSpacer, true);
        }


        /// <summary>
        /// the repetition {}
        /// </summary>
        public void Visit(GraphRepetitionElement ast)
        {
            /*
             * +------->--------+
             * |                |
             * +-<-Expression-<-+
             */

            // temporaries
            var x = XPos; var y = YPos; var liney = LineY;
            var x2 = x + ast.Expression.Size.Width + GenSpacer * 2;
            var liney2 = liney + FactorSpacer + GetSize(TokenClass.Empty).Height;

            // the lines
            ctx.DrawLine(linePen, x, liney, x, liney2);
            ctx.DrawLine(linePen, x2, liney, x2, liney2);
            ctx.DrawLine(linePen, x, liney2, x2, liney2);

            // empty line arrow
            XPos += (ast.Size.Width / 2 - GenSpacer / 2);
            DrawArrow(GenSpacer, true);

            // expression left arrow
            XPos = x; YPos = liney2 - LineMargin; LineY = liney2;
            DrawArrow(GenSpacer, false);

            // the expression itself
            bool dirTmp = ForwardArrows;
            ForwardArrows = !ForwardArrows;
            ast.Expression.Accept(this);
            ForwardArrows = dirTmp;

            // the right side arrow
            XPos = x + GenSpacer + ast.Expression.Size.Width;
            YPos = liney2 - LineMargin;
            LineY = liney2;
            DrawArrow(GenSpacer, false);
        }

        #endregion


        #region Helper Methods

        /// <summary>
        /// Draw the arrow with default line brush
        /// </summary>
        private void DrawArrow(int width, bool forward)
        {
            DrawArrow(width, forward, lineBrush);
        }


        /// <summary>
        /// Draw an arrow with specific brush
        /// </summary>
        /// <param name="p"></param>
        private void DrawArrow(int width, bool forward, Brush brush)
        {
            var a_x = XPos + (width / 2 - ArrowWidth / 2);

            bool b = ForwardArrows ? forward : !forward;

            Point[] points = null;
            if (b)
            {
                points = new Point[]{
                    new Point(a_x, LineY - ArrowWidth / 2),
                    new Point(a_x + ArrowWidth, LineY),
                    new Point(a_x, LineY + ArrowWidth / 2)
                };
            }
            else
            {
                points = new Point[]{
                    new Point(a_x + ArrowWidth, LineY - ArrowWidth / 2),
                    new Point(a_x, LineY),
                    new Point(a_x + ArrowWidth, LineY + ArrowWidth / 2)
                };
            }
            ctx.FillPolygon(brush, points);
            ctx.DrawPolygon(linePen, points);
            XPos += width;
        }
        #endregion
    }
}
